package com.jwong.client.rest;


import com.jwong.client.feign.AuthorizationFeignClient;
import com.jwong.client.model.AccessTokenUserHolder;
import com.jwong.client.model.SysUser;
import com.jwong.client.service.SmsService;
import com.jwong.client.service.UserService;
import com.jwong.common.entity.AccessToken;
import com.jwong.common.entity.FeignResponseErrorBody;
import com.jwong.common.entity.SmsCode;
import com.jwong.common.entity.SmsResponse;
import com.jwong.common.entity.vo.Result;
import com.jwong.common.exception.BaseErrorCode;
import com.jwong.common.util.Constants;
import com.jwong.common.util.RedisKeyGenerator;
import com.jwong.common.util.StringPool;
import com.jwong.common.util.StringUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.util.encoders.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.PropertyResolver;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;

@Api(tags = "APP用户登录模块")
@Slf4j
@RestController
public class UserLoginController {

    private static final String GRANT_TYPE_PASSWORD = "password";
    private static final String GRANT_TYPE_SMS_CODE = "sms_code";

    @Autowired
    private PropertyResolver propertyResolver;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private AuthorizationFeignClient authorizationFeignClient;

    @Autowired
    private SmsService smsService;

    @Autowired
    private UserService userService;

    @ApiOperation(value = "用户名[或手机号]密码登录")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "username", value = "用户名或手机号", required = true, paramType = "query"),
            @ApiImplicitParam(name = "password", value = "密码", required = true, paramType = "query"),
            @ApiImplicitParam(name = "appClientId", value = "客户端ID", required = true, paramType = "query", defaultValue = "app-teacher")
    })
    @PostMapping(value = "/login", produces = MediaType.APPLICATION_JSON_VALUE)
    public Result<AccessTokenUserHolder> username_login(@RequestParam("username") String username,
                                              @RequestParam("password") String password,
                                              @RequestParam("appClientId") String appClientId) {
        if (StringUtils.isBlank(username)) {
            log.error("Input username can be not null.");
            return Result.fail(BaseErrorCode.MOBILE_NUMBER_CAN_NOT_BE_NULL);
        }
        return login(username, password, appClientId, GRANT_TYPE_PASSWORD);
    }

    @ApiOperation(value = "短信验证码登录")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "mobile", value = "手机号", required = true, paramType = "query"),
            @ApiImplicitParam(name = "smsCode", value = "短信验证码", required = true, paramType = "query"),
            @ApiImplicitParam(name = "appClientId", value = "客户端ID", required = true, paramType = "query", defaultValue = "app-teacher")
    })
    @PostMapping(value = "/sms-login", produces = MediaType.APPLICATION_JSON_VALUE)
    public Result<AccessTokenUserHolder> sms_login(@RequestParam("mobile") String mobile,
                                                   @RequestParam("smsCode") String smsCode,
                                                   @RequestParam("appClientId") String appClientId) {
        if (StringUtils.isBlank(mobile)) {
            log.error("SmsCode login : input mobile can be not null.");
            return Result.fail(BaseErrorCode.MOBILE_NUMBER_CAN_NOT_BE_NULL);
        }
        // 家长端用户第一次使用短信验证码登录时，判断用户是否存在，不存在则创建用户？
        if (Constants.APP_PARENT_CLIENT.equals(appClientId)) {
            //userService.createUserParent(mobile, weiCharUserName, weiCharPortrait);
        }
        return login(mobile, smsCode, appClientId, GRANT_TYPE_SMS_CODE);
    }

    private Result<AccessTokenUserHolder> login(String principal, String credentials, String appClientId,
                                            String grant_type) {
        try {
            String clientIdKey = String.format("${%s.clientId}", appClientId);
            String clientId = propertyResolver.resolvePlaceholders(clientIdKey);

            String clientSecretKey = String.format("${%s.clientSecret}", appClientId);
            String clientSecret = propertyResolver.resolvePlaceholders(clientSecretKey);

            byte[] encode = Base64.encode(String.format("%s:%s", clientId, clientSecret).getBytes());
            String authorization = String.format("Basic %s", new String(encode, StandardCharsets.UTF_8));

            ResponseEntity<Object> response = tokenResponse(principal, credentials, grant_type, authorization);
            Object responseBody = response.getBody();

            if (responseBody instanceof FeignResponseErrorBody) {
                FeignResponseErrorBody errorBody = (FeignResponseErrorBody)responseBody;
                log.error("Login failed, error: {}, errorDescription: {}", errorBody.getError(),
                        errorBody.getErrorDescription());
                return Result.fail(errorBody.getErrorDescription());
            }
            if (responseBody instanceof Map) {
                if (response.getStatusCode().is2xxSuccessful()) {
                    Map<String, Object> tokenMap = (Map<String, Object>) responseBody;
                    AccessToken accessToken = buildAccessToken(tokenMap);
                    AccessTokenUserHolder accessTokenUserHolder = makeSuccessHolder(accessToken, principal, appClientId);
                    return Result.success(accessTokenUserHolder);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("Login failed, cause: {}", e.getMessage());
            return Result.fail(BaseErrorCode.LOGIN_ERROR);
        }
        return Result.fail(BaseErrorCode.LOGIN_ERROR);
    }

    private AccessTokenUserHolder makeSuccessHolder(AccessToken accessToken, String principal, String appClientId) {
        switch(appClientId) {
            case Constants.APP_TEACHER_CLIENT:
                SysUser user = this.userService.findUserByMobileOrUsername(principal);
                return new AccessTokenUserHolder(accessToken, user);
            default: return new AccessTokenUserHolder(accessToken);
        }
    }

    private  ResponseEntity<Object> tokenResponse(String principal, String credentials, String grant_type,
                                                             String authorization) throws Exception {
        switch (grant_type) {
            case GRANT_TYPE_PASSWORD: return authorizationFeignClient.tokenPasswordUsername(
                    principal, credentials, grant_type, authorization);
            case GRANT_TYPE_SMS_CODE: return authorizationFeignClient.tokenSmsCode(
                    principal, credentials, grant_type, authorization);
            default: return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    private AccessToken buildAccessToken(Map<String, Object> tokenMap) {
        AccessToken.AccessTokenBuilder builder = AccessToken.builder();
        Map<String, Object> additional = new HashMap<>();
        tokenMap.forEach((key, value) -> {
            switch (key) {
                case "access_token":
                    builder.accessToken(StringUtils.toString(value));
                    break;
                case "refresh_token":
                    builder.refreshToken(StringUtils.toString(value));
                    break;
                case "token_type":
                    builder.tokenType(StringUtils.toString(value));
                    break;
                case "expires_in":
                    builder.expiresIn(Long.parseLong(StringUtils.toString(value)));
                    break;
                case "scope":
                    builder.scope(Arrays.stream(StringUtils.toString(value).split(StringPool.SPACE))
                            .collect(Collectors.toSet()));
                    break;
                default: additional.put(key, value); break;
            }
        });
        builder.additional(additional);
        return builder.build();
    }

    @ApiOperation(value = "获取短信验证码")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "mobile", value = "手机号", required = true, paramType = "path"),
    })
    @GetMapping(value = "sms-code/{mobile}", produces = MediaType.APPLICATION_JSON_VALUE)
    public Result<SmsResponse> smsCode(@PathVariable("mobile") String mobile) {
        SmsCode smsCode = new SmsCode(generateCode(), 60 * 1000 * 10, mobile);
        return sendSmsCode(mobile, smsCode, () -> smsService.sendSingle(smsCode));
    }

    @ApiOperation(value = "获取语音短信验证码")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "mobile", value = "手机号", required = true, paramType = "path"),
    })
    @GetMapping(value = "sms-code/voice/{mobile}", produces = MediaType.APPLICATION_JSON_VALUE)
    public Result<SmsResponse> smsVoice(@PathVariable("mobile") String mobile) {
        SmsCode smsCode = new SmsCode(generateCode(), 60 * 1000 * 10, mobile);
        return sendSmsCode(mobile, smsCode, () -> smsService.sendVoice(smsCode));
    }

    private Result<SmsResponse> sendSmsCode(String mobile, SmsCode newCode, Supplier<Result<SmsResponse>> supplier) {
        if (StringUtils.isBlank(mobile)) {
            log.error("Getting sms code: input mobile is null.");
            return Result.fail(BaseErrorCode.MOBILE_NUMBER_CAN_NOT_BE_NULL);
        }
        try {
            String codeFailTimesKey = RedisKeyGenerator.smsCodeFailTimesKey(mobile);
            BoundValueOperations<String, Integer> codeFailOps = redisTemplate.boundValueOps(codeFailTimesKey);
            Integer codeFailTimes = codeFailOps.get();
            if (codeFailTimes != null && codeFailTimes >= 5) {
                log.error("Getting sms code: fail times[{}] threshold trigger [{}].", codeFailTimes, mobile);
                return Result.fail(BaseErrorCode.SMS_CODE_FAIL_TIMES_THRESHOLD_TRIGGER);
            }

            String smsCodeKey = RedisKeyGenerator.smsCodeKey(mobile);
            BoundValueOperations<String, SmsCode> valueOperations = redisTemplate.boundValueOps(smsCodeKey);
            SmsCode smsCodeInCache = valueOperations.get();
            if (smsCodeInCache != null && !smsCodeInCache.isExpired()) {
                return Result.fail(BaseErrorCode.SMS_CODE_IS_NOT_EXPIRE);
            }

            Result<SmsResponse> smsResponse = supplier.get();
            if (smsResponse.isFail()) return smsResponse;

            if (codeFailTimes == null || codeFailTimes == 0) {
                codeFailOps.set(1,60 * 10 * 1000, TimeUnit.MILLISECONDS);
            } else {
                codeFailOps.increment();
            }
            valueOperations.set(newCode,60 * 10 * 1000, TimeUnit.MILLISECONDS);
            log.info("Getting sms code: the code[{}] send to mobile[{}].", newCode.getCode(), mobile);
            return smsResponse;
        } catch (Exception e) {
            log.error("Fail getting sms code, case: {}", e.getMessage(), e.fillInStackTrace());
            return Result.fail(BaseErrorCode.SMS_CODE_GETTING_ERROR);
        }
    }

    @ApiOperation(value = "短信验证码是否正确")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "mobile", value = "手机号", required = true, paramType = "query"),
            @ApiImplicitParam(name = "smsCode", value = "验证码", required = true, paramType = "query"),
    })
    @GetMapping("/smsCode/conform")
    public Result<Void> conformSmsCode(@RequestParam("smsCode") String smsCode, @RequestParam("mobile") String mobile) {
        return smsService.conformSmsCode(smsCode, mobile);
    }

    private String generateCode() {
        Random random = new Random();
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < 6; i++) {
            stringBuilder.append(Math.abs(random.nextInt() % 10));
        }
        return stringBuilder.toString();
    }

}
